

class DevConsole

  @@commands ||= []

  KNOWN_TYPES = [
    Hash, Array, Fixnum, NilClass,
    TrueClass, FalseClass, String, Symbol,
    Class, Module,
    Time
  ]

  def initialize
    @log = []
  end

  def clear
    @log = []
  end

  def exec code
    @@commands << code
    log '> '+code, :code
    begin
      result = hilite instance_eval(code)
    rescue SyntaxError => e
      log_exception e
    rescue RuntimeError => e
      log_exception e
    rescue NameError => e
      log_exception e
    rescue ArgumentError => e
      log_exception e
    rescue TypeError => e
      log_exception e
    rescue Exception => e
      log_exception e
    end
  end

  def log_exception e
    log CGI.escapeHTML(e.message), :exception_message

    trace = []

    e.backtrace.each { |line|
      if line.include? __FILE__
        break
      end
      trace << "  "+line
    }
    log trace.join("\n"), :exception_trace
    nil
  end

  def log what = nil, kind = :result
    if what
      @log << [what, kind]
    end
    @log
  end

  def hilite something, level = 0, already_hilited = []

    if already_hilited.include? something
      return "(R)"
    else
      already_hilited = already_hilited.dup << something
    end

    if level == 12
      return something.class.to_s
    end
    level += 1

    interesting_class = something.class.ancestors.select { |a|
      KNOWN_TYPES.include? a
    }.first

    is_known = !!interesting_class


    if is_known
      if something.class == Hash
        if something.keys.length > 0
          items = something.map {|k, v|
            [hilite(k, level) +' <span class="op">=></span> ' + hilite(v, level, already_hilited),", "]
          }
          if items.length > 0
            items.last[1] = ''
          end
          ul = Element.new(:ul)
          items.each { |item|
            ul.append_child Element.new(:li, item.to_s)
          }
          inspect = "{"+ul.to_s+"}"
        else
          inspect = "{}"
        end
      elsif something.class == Array
        if something.length > 0
          items = something.map {|i|
            [hilite(i, level, already_hilited), ", "]
          }
          if items.length > 0
            items.last[1] = ''
          end
          ul = Element.new(:ul)
          items.each { |item|
            ul.append_child Element.new(:li, item.to_s)
          }
          inspect = "["+ul.to_s+"]"
        else
          inspect = "[]"
        end
      elsif something.class == Module
        inspect = CGI.escapeHTML(something.to_s)
      else
        inspect = CGI.escapeHTML(something.inspect)
      end
      inspect = Element.new(
        :span, inspect, :class => interesting_class.to_s
      ).to_s
      if (level > 2) && (inspect.length > 10000)
        return something.class.to_s+"(too long)"
      end
      return inspect
    end

    div = Element.new(:div, :class => 'Object')
    label = hilite something.class
    unless something.respond_to? :ancestors
      label += ' < '+ (hilite something.class.ancestors[1])
    end
    div.append_child(Element.new(:span, label))
    ul = Element.new(:ul)
    something.instance_variables.each { |var|
      if var[1,1] == '_'
        next
      end
      li = Element.new(:li, '<span class="name">'+var+":</span>")
      li.append_child(hilite something.instance_variable_get(var), level, already_hilited)
      ul.append_child(li)
    }
    div.append_child ul
    html = div.to_s

    if (level > 4) && (html.length > (1000 / level))
      return something.class.to_s+" (...)"
    end
    html
  end

end









class Terminal < Controller

  @@log ||= []
  @@devconsole ||= DevConsole.new


  def index e
    events = []
    if e.params['command']
      on_perform(log = Event.new(nil, :command => e.params['command']))
      events = log.response
    end

    Layout.render "<script>
      console_init({
       event_url:window.location,
       commands: #{@@log.to_json},
       log: #{events.map(&:to_hash).to_json()}
      });
    </script>"
  end

  def on_perform e
    time = Time.now.to_i
    @@log << e.command
    if @@log.length > 100
      @@log.shift
    end
    result = @@devconsole.exec e.command
    @@devconsole.log.each { |record|
      e.respond(
        'log',
        :string => record[0],
        :kind => record[1]
      )
    }
    @@devconsole.clear
    begin
      list = StandardOutput.instance.list(time)

      if list.length == 0
        @@devconsole.log result
      end

      StandardOutput.instance.list(time).each { |line|
        if line[0] == :p
          e.respond(
            'log',
            :string => line[1],
            :kind => 'result'
          )
        else
          e.respond(
            'log',
            :string => line[1].join("<br/>"),
            :kind => 'puts'
          )
        end
      }
    end
    #@@devconsole.log result
    @@devconsole.log.each { |record|
      e.respond(
        'log',
        :string => record[0],
        :kind => record[1]
      )
    }
    @@devconsole.clear
  end

  class Layout
    def self.render content
    <<HTML
<style>
.Console { font-family:consolas, 'lucida console'; }

.Console .log, .Console .command, .Console .command .input {
  font-size:1.5em;
  background:black;
  color:white;
  font-family:consolas, 'lucida console';
}
.Console .log {
  width:100%;
  height:100%;
  font-size:1em;
  background:#151505;
  overflow:auto;
}
.Console .command {
  background:black;
  color:white;
  width:100%;
}
.Console .command .input {
  font-size:1em;
  border:none;
  background:none;
  width:100%;
  padding:0;
  margin:0;
  margin-top:2px;
  overflow:hidden;
}
.Console .command .input_wrapper {
  padding-left:1em;
  font-size:1em;
  border:none;
}
.Console .command .arrow {
  line-height:1.4em;
  padding-left:0.25em;
  float:left;
}
.Console .result { color:gray; padding-left:0.5em; }
.Console, .Console tr, .Console td { padding:0; margin:0; }
.Console td { height:100%; }
.Console .log .String { color:#1f1;}
.Console .log .Symbol { color:#69f; }
.Console .log .Fixnum { color:yellow; }
.Console .log .Array { /*color:white;*/ }
.Console .log .name { color:#99f; }
.Console .log .NilClass,
.Console .log .TrueClass,
.Console .log .FalseClass,
.Console .log .Class, .Console .log .Module {
  color:#f80;
}
.Console .log li {
  padding-left:0;
}
.Console .log ul {
  margin:0;
  padding-left:20px;
}

.Console .log .Object li {
  list-style-type:none;
}
.Console .log .Array > li {
  list-style-type:none;
  display:inline;
}
.Console .log .Array li, .Console .log .Array ul {
  margin:0;
  display:inline;
  padding:0;
}
.Console .comma {
  /*float:left*/
}
.Console .Object {
  padding-left:10px;
}
.Console .log .Object ul, .Console .log .Object li,  .Console .log .Hash li,  .Console .log .Hash ul {
  display:block;
  padding-left:10px;
}
.Console .log .op {
  color:#f60;
}
</style>

<script>
console_init = function(app_params) {

  YUI().use("node", "json", 'io', function(Y) {
    var e = Y.Node.create;

    var send = function(eventName, params, callback) {
        params = params ? params : {};

        var json = Y.JSON.stringify({
               name: eventName,
               params: params
            });

        Y.io(app_params.event_url,{
          method: "POST",
          data: 'json='+encodeURIComponent(json),
          on: {
            success: function(id, response) {
              var events = Y.JSON.parse(response.responseText);

              for (var e = 0; e < events.length; e++) {
                var event = events[e];
                Y.fire(event.name, event);
              }

              if (typeof(callback) != 'undefined') {
                callback({event:responseData.event});
              }
            },
            fail: function() {
              Y.log(Y.JSON.stringify(arguments[1]), 'error');
            }
          }
        });
    }



    var Console = function(root) {
      var self = this;
      self.root = root;
      self.commands = [];

      this.updateStdout = function() {
        send('get_stdout', {from: null}, function(e) {
          console.log(e);
        })
      }

      this.render = function() {
        self.node = e('<div class="Console"'+
          '>');
        var top_tr = self.node.appendChild(e('<div></div>'));
        var top_td = top_tr.appendChild(e('<div></div>'));
        self.logEl = e('<div class="log"></div>');
        top_td.appendChild(self.logEl);

        var bottom_tr = self.node.appendChild(e('<div></div>'));
        var bottom_td = bottom_tr.appendChild(e('<div class="bottom"></div>'));

        self.cmd_box = e('<div class="command"></div>');
        bottom_td.appendChild(self.cmd_box);

        self.total_bottom = e('<div class="total_bottom"></div>');
        bottom_td.appendChild(self.total_bottom);

        self.cmd_box.appendChild(e('<span class="arrow">></span>'));
        var input_wrapper = e('<div class="input_wrapper"></div>');
        self.input = e('<textarea class="input" type="text" autocomplete="off"></textarea>');
        input_wrapper.appendChild(self.input);
        self.cmd_box.appendChild(input_wrapper);
        self.inputDomNode = Y.Node.getDOMNode(self.input);
        return self.node;
      }

      this.subscribe = function() {

        self.logEl.on('click', function() {
          //self.input.focus();
        });

        self.input.on('keydown', function(e) {
          if (e.keyCode == 38 || e.keyCode == 40) {
            var multiple_lines = (self.input.get('value').split("\\n").length > 1);
            if ((!multiple_lines || e.ctrlKey) && e.keyCode == 38) {
              self.lastCommand();
            }
            if ((!multiple_lines || e.ctrlKey) && e.keyCode == 40) {
              self.nextCommand();
            }
          }
        });

        self.input.on('keypress', function(e) {
          if (e.keyCode == 13 || e.keyCode == 10) {
            var multiple_lines = (self.input.get('value').split("\\n").length > 1);
            if ((multiple_lines && e.ctrlKey) || (!multiple_lines && !e.ctrlKey)) {
              self.perform(self.input.get('value'));
              self.input.set('value', '');
              e.preventDefault();
            } else if(e.ctrlKey) {
              self.insert(" \\n ");
              var lines = self.inputDomNode.value.split("\\n").length;
              lines = Math.max(lines, 2);
              self.inputDomNode.rows = lines+1;
            }
          }
        });

        self.input.on('keyup', function(e) {
            var lines = self.inputDomNode.value.split("\\n").length;
            lines = Math.max(lines, 2);
            self.inputDomNode.rows = lines;
            self.total_bottom.scrollIntoView();
        })
      }

      this.lastCommand = function() {
        if (self.historyPosition > 0) {
          self.historyPosition--;
          self.input.set('value', self.commands[self.historyPosition]);
        }
      }

      this.nextCommand = function() {
          if (self.historyPosition < self.commands.length) {
            self.historyPosition++;
            var command = self.commands[self.historyPosition];
            command = command ? command : '';
            self.input.set('value', command);
          }
      }

      this.insert = function(value) {
        var myField = Y.Node.getDOMNode(self.input);
        var myValue = value

        //IE support
        if (document.selection) {
          myField.focus();
          sel = document.selection.createRange();
          sel.text = myValue;
          myField.focus();
        }
        //MOZILLA/NETSCAPE support
        else if (myField.selectionStart || myField.selectionStart == '0') {
          var startPos = myField.selectionStart;
          var endPos = myField.selectionEnd;
          myField.value = myField.value.substring(0, startPos)
          + myValue
          + myField.value.substring(endPos, myField.value.length);
          myField.focus();
          myField.selectionStart = startPos + myValue.length;
          myField.selectionEnd = startPos + myValue.length;
        } else {
          myField.value += myValue;
          myField.focus();
        }
      }

      this.log = function(string, kind) {
        var line = self.logEl.appendChild(e('<div></div>'));
        line.set('innerHTML', string);
        line.addClass(kind);
        self.total_bottom.scrollIntoView();
      }

      Y.on('log', function(e) {
        self.log(e.string, e.kind)
      });

      this.perform = function(command) {
        //alert(command);
        self.commands.push(command);
        self.historyPosition = self.commands.length;
        send('perform', {command : command});
      }

      this.setCommands = function(commands) {
        self.commands = commands;
        self.historyPosition = self.commands.length;
      }

      this.focus = function() {
        self.input.focus();
      }

      self.root.appendChild(self.render());
      self.subscribe();
    }

    //var myconsole = new Console(Y.get('#console_here'));
    myconsole = new Console(Y.get('#console_here'));
    myconsole.setCommands(app_params.commands);
    myconsole.focus();


    for (var i = 0; i <app_params.log.length; i++) {
      var event = app_params.log[i];
      Y.fire(event.name, event);
    }

  });
}
</script>


  <div id="console_here" style="height:100%"></div>
  #{content}

HTML
    end
  end
end

